#!/bin/bash
#
# 评测提交的脚本
#
# 用法：$0 <datadir> <timelimit> <chrootdir> <workdir> <run-uuid> <run> <compare>
#
# <datadir>      包含数据文件的文件夹的绝对路径
# <timelimit>    运行时间限制，格式为 %d:%d，如 1:3 表示测试点时间限制
#                为 1s，如果运行时间超过 3s 则结束程序
# <chrootdir>    子环境
# <workdir>      程序的工作文件夹，为了保证安全，请务必将运行路径设置
#                为空文件夹，特别是保证不可以包含标准输出文件
# <run-uuid>     运行的 uuid，用于索引运行文件夹位置
# <run>          运行程序的脚本的文件夹
# <compare>      比较程序/脚本文件夹的文件夹
#
# 必须包含的环境变量：
#   RUNGUARD        runguard 的路径
#   RUNUSER         选手程序运行的账户
#   RUNGROUP        选手程序运行的账户组
#   SCRIPTMEMLIMIT  比较脚本运行内存限制
#   SCRIPTTIMELIMIT 比较脚本执行时间
#   SCRIPTFILELIMIT 比较脚本输出限制
#
# 可选环境变量
#   MEMLIMIT     运行内存限制，单位为 KB
#   PROCLIMIT    进程数限制
#   FILELIMIT    文件写入限制，单位为 KB
#
# 默认的比较脚本都可以放在配置服务中

# 导入比较脚本，功能是初始化日志、处理命令行参数、并对参数进行初步检查
. "$JUDGE_UTILS/check_helper.sh"

TESTIN="$DATADIR/input"
TESTOUT="$DATADIR/output"
[ -d "$TESTIN" ] || error "input data does not exist: $TESTIN"
[ -d "$TESTOUT" ] || error "output data does not exist: $TESTOUT"

PROGRAM="compile/run"
[ -x "$WORKDIR/$PROGRAM" ] || error "Program does not exist"
[ -d "$COMPARE_SCRIPT" ] || error "Compare script does not exist"
[ -d "$RUN_SCRIPT" ] || error "Run script does not exist"

# 设置脚本权限，确保可以直接运行
chmod +x "$RUN_SCRIPT/run"
chmod +x "$COMPARE_SCRIPT/run"

touch program.meta program.err
touch compare.meta compare.err

mkdir -m 0777 -p run # 运行的临时文件都在这里
mkdir -m 0755 -p work
mkdir -m 0777 -p work/judge
mkdir -m 0755 -p work/compare
mkdir -m 0755 -p work/testin
mkdir -m 0755 -p work/testout
mkdir -m 0777 -p work/run
mkdir -m 0777 -p ofs
mkdir -m 0777 -p ofs/merged
mkdir -m 0777 -p ofs/judge
mkdir -m 0755 -p merged
# 将测试数据文件夹（内含输入数据，且其中 testdata.in 为标准输入数据文件名），编译好的程序，运行文件夹通过 overlayfs 绑定
$GAINROOT mount -t overlay overlay -olowerdir="$CHROOTDIR",upperdir=work,workdir=ofs/merged merged
$GAINROOT mount -t overlay overlay -olowerdir="$WORKDIR/compile":"$TESTIN",upperdir=run,workdir=ofs/judge merged/judge
$GAINROOT mount --bind -o ro "$RUN_SCRIPT" merged/run

chroot_start "$CHROOTDIR" merged

# 我们不检查选手程序的返回值，比如 C 程序的 main 函数没有写 return 会导致返回值非零，这种不是崩溃导致的
runcheck $GAINROOT "$RUNGUARD" ${DEBUG:+-v} $CPUSET_OPT $MEMLIMIT_OPT $FILELIMIT_OPT $PROCLIMIT_OPT \
    --root merged \
    --work /judge \
    --no-core-dumps \
    --user "$RUNUSER" \
    --group "$RUNGROUP" \
    --wall-time "$TIMELIMIT" \
    --standard-error-file program.err \
    --out-meta program.meta \
    -VONLINE_JUDGE=1 -- \
    /run/run testdata.in testdata.out /judge/run "$@"

# 比较选手程序输出
echo "Comparing output"
rm -rf work/feedback || /bin/true
mkdir -m 0777 -p work/feedback
$GAINROOT mount --bind -o ro "$TESTIN" merged/testin
$GAINROOT mount --bind -o ro "$TESTOUT" merged/testout
$GAINROOT mount --bind -o ro "$COMPARE_SCRIPT" merged/compare

runcheck $GAINROOT "$RUNGUARD" ${DEBUG:+-v} $CPUSET_OPT \
    --root merged \
    --work /judge \
    --no-core-dumps \
    --user "$RUNUSER" \
    --group "$RUNGROUP" \
    --memory-limit "$SCRIPTMEMLIMIT" \
    "$OPTTIME" "$SCRIPTTIMELIMIT" \
    --file-limit "$SCRIPTFILELIMIT" \
    --standard-output-file compare.out \
    --standard-error-file compare.err \
    --out-meta compare.meta \
    -VONLINE_JUDGE=1 \
    /compare/run /testin /judge /testout /feedback

chroot_stop "$CHROOTDIR" merged

# 删除挂载点，因为我们已经确保有用的数据在 $WORKDIR/run-$uuid 中，因此删除挂载点即可。
force_umount merged/judge
force_umount merged/compare
force_umount merged/testin
force_umount merged/testout
force_umount merged/run
force_umount merged
mv work/feedback feedback
rm -rf merged
rm -rf work
rm -rf ofs
# RUNDIR 还剩下 compare.meta, compare.out, compare.err, program.meta, program.err, system.out 供评测客户端检查
# RUNDIR 由评测客户端删除

# Make sure that all feedback files are owned by the current
# user/group, so that we can append content.
$GAINROOT chown -R "$(id -un):" feedback
chmod -R go-w feedback

# 记录比较器的标准输出
if [ -s compare.out ]; then
	printf "\\n---------- output validator stdout messages ----------\\n"
	cat compare.out
fi

# 记录比较器的标准错误流
if [ -s compare.err ]; then
	printf "\\n---------- output validator stderr messages ----------\\n"
	cat compare.err
fi

echo "checking compare script exit-status: $exitcode"
if grep '^time-result: .*timelimit' compare.meta >/dev/null 2>&1; then
    echo "Comparing aborted after $SCRIPTTIMELIMIT seconds"
    cleanexit ${E_COMPARE_ERROR:-1}
fi

if grep -E '^internal-error: .+$' compare.meta >/dev/null 2>&1; then
    echo "Internal Error"
    echo "$resource_usage"
    cleanexit ${E_INTERNAL_ERROR:-1}
fi

if [ ! -r program.meta ]; then
    error "'program.meta' is not readable"
fi

echo "Checking program run status"
if [ ! -s program.meta ]; then
    printf "\n****************runguard crash*****************\n"
    cleanexit ${E_INTERNAL_ERROR:--1}
fi
read_metadata program.meta

if grep -E '^internal-error: .+$' program.meta >/dev/null 2>&1; then
    echo "Internal Error"
    echo "$resource_usage"
    cleanexit ${E_INTERNAL_ERROR:-1}
fi

if grep '^time-result: .*timelimit' program.meta >/dev/null 2>&1; then
    echo "Time Limit Exceeded"
    echo "$resource_usage"
    cleanexit ${E_TIME_LIMIT:-1}
fi

if grep '^memory-result: oom' program.meta >/dev/null 2>&1; then
    echo "Memory Limit Exceeded"
    echo "$resource_usage"
    cleanexit ${E_MEM_LIMIT:-1}
fi

if grep -E '^output-truncated: ([a-z]+,)*stdout(,[a-z]+)*' program.meta >/dev/null 2>&1; then
    echo "Output Limit Exceeded"
    echo "$resource_usage"
    cleanexit ${E_OUTPUT_LIMIT:-1}
fi

if [ ! -z $signal ]; then
    case $signal in
        11) # SIGSEGV
            echo "Segmentation Fault"
            echo "$resource_usage"
            cleanexit ${E_SEG_FAULT:-1}
            ;;
        8) # SIGFPE
            echo "Floating Point Exception"
            echo "$resource_usage"
            cleanexit ${E_FLOATING_POINT:-1}
            ;;
        9) # SIGKILL
            echo "Memory Limit Exceeded"
            echo "$resource_usage"
            cleanexit ${E_MEM_LIMIT:-1}
            ;;
        *)
            echo "Runtime Error"
            echo "$resource_usage"
            cleanexit ${E_RUNTIME_ERROR:-1}
            ;;
    esac
fi

# FIXME: C 语言程序可能会因为没有写 return 0; 导致非零
# 返回值而误判为 Runtime Error
# 经过测试，现在 gcc/g++ 会自动解决 main 函数没有 return 0 的问题，暂时不需要解决
if [ "$progexit" -ne 0 ]; then
    echo "Non-zero exitcode $progexit"
    echo "$resource_usage"
    cleanexit ${E_RUNTIME_ERROR:-1}
fi

if [ $exitcode -eq $RESULT_PC ] && [ ! -f "$RUNDIR/feedback/score.txt" ]; then
    echo "Compare script reports partial correct without score record."
    cleanexit ${E_COMPARE_ERROR:-1}
fi

case $exitcode in
    $RESULT_AC)
        echo "Accepted"
        echo "$resource_usage"
        cleanexit ${E_ACCEPTED:-1}
        ;;
    $RESULT_WA)
        echo "Wrong Answer"
        echo "$resource_usage"
        cleanexit ${E_WRONG_ANSWER:-1}
        ;;
    $RESULT_PE)
        echo "Presentation Error"
        echo "$resource_usage"
        cleanexit ${E_PRESENTATION_ERROR:-1}
        ;;
    $RESULT_PC)
        echo "Partial Correct"
        echo "$resouce_usage"
        cleanexit ${E_PARTIAL_CORRECT:-1}
        ;;
    *)
        echo "Comparing failed with exitcode $exitcode"
        cleanexit ${E_COMPARE_ERROR:-1}
        ;;
esac
